﻿using System;
using System.Collections.Concurrent;
using System.Linq;
using TelerikMvcGridCustomBindingHelper.Util;

namespace TelerikMvcGridCustomBindingHelper.Mapper
{
    public class ValueResolvers : ConcurrentDictionary<ValueResolverKey, IValueResolver>
    {
        public bool HasResolverFor<TEntity, TViewModel>(GridPropertyInfo gridPropertyInfo, Type valueType, bool searshForQueryValueResolvers = false)
        {
            Contract.Expect(() => gridPropertyInfo).IsNotNull();

            if (searshForQueryValueResolvers)
            {
                return Values.Where(resolver => resolver is IQueryValueResolver).Any(resolver => _targetPropertyMatch((IQueryValueResolver)resolver, gridPropertyInfo));
            }

            var result = ContainsKey(ValueResolverKey.New(typeof(TEntity), typeof(TViewModel), gridPropertyInfo));

            if (result == false && valueType != null)
            {
                result = ContainsKey(ValueResolverKey.GlobalKey(valueType, gridPropertyInfo.PropertyType));
            }

            if (result == false && (gridPropertyInfo.PropertyType != null && gridPropertyInfo.ViewModelPropertyType != null))
            {
                result = ContainsKey(ValueResolverKey.GlobalKey(gridPropertyInfo.ViewModelPropertyType, gridPropertyInfo.PropertyType));
            }

            return result;
        }

        public object Resolve<TEntity, TViewModel>(GridPropertyInfo gridPropertyInfo, object value, bool searshForQueryValueResolvers = false)
        {
            IValueResolver valueResolver;

            var rc = new ResultContext(typeof(TEntity), typeof(TViewModel), value);

            var tryGetValue = TryGetValue(ValueResolverKey.New(rc.EntityType, rc.ViewModelType, gridPropertyInfo), out valueResolver);

            if (tryGetValue == false && value != null)
            {
                tryGetValue = TryGetValue(ValueResolverKey.GlobalKey(value.GetType(), gridPropertyInfo.PropertyType), out valueResolver);
            }

            if (tryGetValue == false && (gridPropertyInfo.PropertyType != null && gridPropertyInfo.ViewModelPropertyType != null))
            {
                tryGetValue = TryGetValue(ValueResolverKey.GlobalKey(gridPropertyInfo.ViewModelPropertyType, gridPropertyInfo.PropertyType), out valueResolver);
            }

            if (searshForQueryValueResolvers)
            {
                valueResolver = Values.Where(resolver => resolver is IQueryValueResolver).FirstOrDefault(resolver => _targetPropertyMatch((IQueryValueResolver)resolver, gridPropertyInfo));
                tryGetValue = valueResolver != null;
            }

            return tryGetValue == false ? value : valueResolver.Resolve(rc);
        }

        readonly Func<IQueryValueResolver, GridPropertyInfo, bool> _targetPropertyMatch = (resolver, gpi) =>
        {
            if (resolver.GetTargetProperty().IsNullOrWhiteSpace())
                return true;

            var match = resolver.GetTargetProperty().Equals(gpi.ViewModelProperty.GetPropertyPath() ?? gpi.PropertyPath);

            if (match)
                return resolver.GetDestinationType() == gpi.PropertyType;

            return false;
        };
    }

    public struct ValueResolverKey : IEquatable<ValueResolverKey>
    {
        public readonly Type EntityType;
        public readonly Type ViewModelType;
        public readonly GridPropertyInfo GridPropertyInfo;

        private ValueResolverKey(Type entityType, Type viewModelType, GridPropertyInfo gridPropertyInfo)
        {
            EntityType = entityType;
            GridPropertyInfo = gridPropertyInfo;
            ViewModelType = viewModelType;
        }

        public static ValueResolverKey New(Type entityType, Type viewModelType, GridPropertyInfo gridPropertyInfo)
        {
            return new ValueResolverKey(entityType, viewModelType, gridPropertyInfo);
        }

        public static ValueResolverKey GlobalKey(Type sourceType, Type destinationType)
        {
            var gpi = new GridPropertyInfo(sourceType, sourceType.FullName);
            return new ValueResolverKey(sourceType, destinationType, gpi);
        }

        public static ValueResolverKey QueryKey(IQueryValueResolver queryValueResolver)
        {
            if (queryValueResolver.GetTargetProperty().IsNullOrWhiteSpace())
                return GlobalKey(queryValueResolver.GetSourceType(), queryValueResolver.GetDestinationType());

            var sourceType = queryValueResolver.GetSourceType();
            var destinationType = queryValueResolver.GetDestinationType();

            var gpi = new GridPropertyInfo(sourceType, queryValueResolver.GetTargetProperty());
            return new ValueResolverKey(sourceType, destinationType, gpi);
        }

        #region IEquatable<ValueResolverKey>

        public bool Equals(ValueResolverKey other)
        {
            return other.EntityType == EntityType
                   && other.ViewModelType == ViewModelType
                   && GridPropertyInfo != null
                   && (other.GridPropertyInfo.PropertyType == GridPropertyInfo.PropertyType
                   && Equals(other.GridPropertyInfo.PropertyPath, GridPropertyInfo.PropertyPath));
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (obj.GetType() != typeof(ValueResolverKey)) return false;
            return Equals((ValueResolverKey)obj);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                var result = ToString().GetHashCode();

                if (EntityType != null) result = (result * 397) ^ EntityType.GetHashCode();

                if (ViewModelType != null) result = (result * 397) ^ ViewModelType.GetHashCode();

                if (GridPropertyInfo != null)
                {
                    if (GridPropertyInfo.PropertyPath != null)
                        result = (result * 397) ^ GridPropertyInfo.PropertyPath.GetHashCode();
                    if (GridPropertyInfo.PropertyType != null)
                        result = (result * 397) ^ GridPropertyInfo.PropertyType.GetHashCode();
                }

                return result;
            }
        }

        #endregion
    }
}